/*
 * Copyright (c) 2023-2023 elsfs Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.elsfs.cloud.common.security.filter;

import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.userdetails.UserCache;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsChecker;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.core.userdetails.cache.NullUserCache;
import org.springframework.util.Assert;

/**
 * 抽象的AuthenticationProvider
 *
 * @author zeng
 */
@Slf4j
public abstract class AbsAuthenticationProvider
    implements AuthenticationProvider, InitializingBean, MessageSourceAware {
  @Setter protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

  @Getter private UserCache userCache = new NullUserCache();

  /** 隐藏未找到用户的异常 */
  @Setter protected boolean hideUserNotFoundExceptions = false;

  // 用户详细信息检查器
  @Getter @Setter
  private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();

  @Getter @Setter
  private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();

  private boolean forcePrincipalAsString = false;
  private final GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

  @Override
  public final void afterPropertiesSet() throws Exception {
    Assert.notNull(this.userCache, "A user cache must be set");
    Assert.notNull(this.messages, "A message source must be set");
    doAfterPropertiesSet();
  }

  protected void doAfterPropertiesSet() throws Exception {}

  protected abstract UserDetails retrieveUser(
      String username, AbstractAuthenticationToken authentication) throws AuthenticationException;

  /**
   * 检查
   *
   * @param userDetails 检查的源数据
   * @param authentication 携带的数据
   * @throws AuthenticationException e
   */
  protected abstract void additionalAuthenticationChecks(
      UserDetails userDetails, AbstractAuthenticationToken authentication)
      throws AuthenticationException;

  @Override
  public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    Assert.isInstanceOf(
        AbstractAuthenticationToken.class,
        authentication,
        () ->
            this.messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.onlySupports",
                "Only AbstractAuthenticationToken is supported"));
    String username = determineUsername(authentication);
    boolean cacheWasUsed = true;
    UserDetails user = this.userCache.getUserFromCache(username);
    if (user == null) {
      cacheWasUsed = false;
      try {
        user = retrieveUser(username, (AbstractAuthenticationToken) authentication);
      } catch (UsernameNotFoundException ex) {
        LOGGER.debug("Failed to find user '" + username + "'");
        if (!this.hideUserNotFoundExceptions) {
          throw ex;
        }
        throw new BadCredentialsException(
            this.messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
      }
      Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
    }
    try {
      this.preAuthenticationChecks.check(user);
      additionalAuthenticationChecks(user, (AbstractAuthenticationToken) authentication);
    } catch (AuthenticationException ex) {
      if (!cacheWasUsed) {
        throw ex;
      }
      // There was a problem, so try again after checking
      // we're using latest data (i.e. not from the cache)
      cacheWasUsed = false;
      user = retrieveUser(username, (AbstractAuthenticationToken) authentication);
      this.preAuthenticationChecks.check(user);
      additionalAuthenticationChecks(user, (AbstractAuthenticationToken) authentication);
    }
    this.postAuthenticationChecks.check(user);
    if (!cacheWasUsed) {
      this.userCache.putUserInCache(user);
    }
    Object principalToReturn = user;
    if (this.forcePrincipalAsString) {
      principalToReturn = user.getUsername();
    }
    return createSuccessAuthentication(principalToReturn, authentication, user);
  }

  protected Authentication createSuccessAuthentication(
      Object principal, Authentication authentication, UserDetails user) {
    // Ensure we return the original credentials the user supplied,
    // so subsequent attempts are successful even with encoded passwords.
    // Also ensure we return the original getDetails(), so that future
    // authentication events after cache expiry contain the details
    UsernamePasswordAuthenticationToken result =
        UsernamePasswordAuthenticationToken.authenticated(
            principal,
            authentication.getCredentials(),
            this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
    result.setDetails(authentication.getDetails());
    LOGGER.debug("Authenticated user");
    return result;
  }

  private String determineUsername(Authentication authentication) {
    return (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
  }

  @Override
  public void setMessageSource(MessageSource messageSource) {
    this.messages = new MessageSourceAccessor(messageSource);
  }

  public void setUserCache(UserCache userCache) {
    this.userCache = userCache;
  }

  @Override
  public boolean supports(Class<?> authentication) {
    return (AbstractAuthenticationToken.class.isAssignableFrom(authentication));
  }

  private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
    @Override
    public void check(UserDetails user) {
      if (!user.isAccountNonLocked()) {
        LOGGER.debug("Failed to authenticate since user account is locked");
        throw new LockedException(
            AbsAuthenticationProvider.this.messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
      }
      if (!user.isEnabled()) {
        LOGGER.debug("Failed to authenticate since user account is disabled");
        throw new DisabledException(
            AbsAuthenticationProvider.this.messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
      }
      if (!user.isAccountNonExpired()) {
        LOGGER.debug("Failed to authenticate since user account has expired");
        throw new AccountExpiredException(
            AbsAuthenticationProvider.this.messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
      }
    }
  }

  private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
    @Override
    public void check(UserDetails user) {
      if (!user.isCredentialsNonExpired()) {
        LOGGER.debug("Failed to authenticate since user account credentials have expired");
        throw new CredentialsExpiredException(
            AbsAuthenticationProvider.this.messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.credentialsExpired",
                "User credentials have expired"));
      }
    }
  }
}
